第22天。最近瘋狂鼻塞,令人身心俱疲。
之前練習都是直接從模式的角度來寫,現在我們從SRP的角度來看這些模式。
首先,代理模式:
在代理模式之前練習中,就已經把預載圖片的行為放到代理物件中,其本體要做的也只有往頁面添加img的tag物件職責而已。
//保持單一功能
var $imgNode = (function() {
var $imgNode = $('<img>').appendTo('body');
return {
setSrc: function(src) {
$imgNode.attr('src', src);
}
}
})();
//預載代理
var proxyImage = (function() {
var img = new Image;
img.onload = function() {
$imgNode.setSrc(this.src);
};
return {
setSrc: function(src) {
var loadingPage = 'loading.jpg';
$imgNode.setSrc(loadingPage);
img.src = src;
}
}
})();
proxyImage.setSrc('https://upload.cc/i1/2018/10/18/uCewa6.png');
把添加元素功能和預載圖片功能分開到獨立的物件中,這兩個物件都各自有一個獨立的目的,在他們各自需求被修改時,都不會互相影響,基本上就符合SRP的精神。
迭代器模式:
在這麼模式的練習中,我們有類似這樣的程式碼
var appendDiv = function (data) {
for (var i = 0; i < data.length; i++) {
$('<div>').html(data[i]).appendTo('body');
}
};
appendDiv([1, 2, 3, 4, 5, 6]);
理論上appendDiv應該就只要負責在body中添加div元素就好,但這邊又多了一個遍歷data的動作。如果我們的data傳入的不是陣列而是物件,那我們就必須修改appendDiv的內容。但是迭代器的功用就是該遍歷data,那該怎辦呢?我們現在就把遍歷data的行為獨立出來
var eachData = function (obj, callback) {
var self = this;
if ($.isArray(obj)) {
obj.forEach(function (data, index) {
callback.call(self, data, index);
});
} else {
for (var key in obj) {
callback.call(self, obj[key], key);
}
}
};
var appendDiv = function (data) {
eachData(data, function (_data, key) {
$('<div>').html(_data).appendTo('body');
});
};
appendDiv([1, 2, 3, 4, 5, 6]);
appendDiv({ a: 1, b: 2, c: 3 });
我們把迭代的功能獨立放進eachData方法中,讓eachData與appendDiv保持獨立,當各自變動時互不影響。
單例模式:
之前用過的範例
var $button = (function() {
return $('<button>').text('登入').appendTo('body');
})();
var $loginLayer;
function createLayer() {
if ($loginLayer) {
return $loginLayer;
} else {
var $layer = $('<div>').text('我是登入畫面').appendTo('body');
$layer.hide();
return $layer;
}
}
function showLayer() {
$loginLayer = createLayer();
$loginLayer.show();
}
$button.click(showLayer);
下面是改為符合SRP的寫法
var $button = (function() {
return $('<button>').text('登入').appendTo('body');
})();
function getSingle(fn) {
var singleItem;
var argTmp = [].slice.call(arguments);
var newArg = (function() {
argTmp.shift();
return argTmp;
})();
return function() {
return singleItem || (singleItem = fn.apply(this, newArg));
}
}
function createLayer(text) {
var $layer = $('<div>').text(text).appendTo('body');
$layer.hide();
return $layer;
}
var createSingleLoginLayer = getSingle(createLayer,'我是登入畫面');
function showLayer() {
createSingleLoginLayer().show();
}
$button.click(showLayer);
我們把製造單例的職責(getSingle)與產生畫面的職責(createLayer)分開來,以便之後的維護與修改,更可以讓物件重複使用。
裝飾者模式:
裝飾者模式會讓每個物件負責單一職責,然後再“裝飾”上去,基本上就符合單一職責原則了,如底下code裡的showLogin與log兩個方法
Function.prototype.after = function(fn) {
var _self = this;
return function() {
var ret = _self.apply(this, arguments);
fn.apply(this, arguments);
return ret;
}
};
var $btn;
(function() {
$btn = $('<body>').text('Login').attr('tag', 'login')
.appendTo('body');
})();
function showLogin() {
console.log('打開登入畫面');
}
function log() {
console.log('回傳tag:' + $(this).attr('tag'));
//省略傳server部分
}
showLogin = showLogin.after(log);
$btn.click(showLogin);
SRP應用最重要的議題就是如何分離職責,準確來說並不一定要將所有的職責分離,如果能確定這些職責的變化會是同時發生的就可以放在一起,例如AJAX請求裡,建立XHR物件與發送XHR這兩個職責就可以放在一起,因為他們幾乎都在一起使用。
另外,有些時候到了需要的再分離職責也是可以的,等這些職責都會發生變化再來分離會比較有拆解的意義。而在拆解的時候也可以將“方便使用”或是“方便維護”作為考量,例如JQUERY裡的attr方法就同時具有賦予值與獲取值的功能,這樣的方法在維護上一定較困難,但使用者使用起來就很方便。
SRP優點在於拆解物件的複雜度,有助於我們維護與重複使用,但也有可能造成我們為了連結這些物件而讓我們整體使用難度增加,甚至我們必須額外增加一些方法才可以讓這些獨立的職責組合起來使用。